package org.eclipse.jface.text;

import org.eclipse.core.runtime.Assert;

import java.util.ArrayList;
import java.util.List;

/**
 * @project: JvIDE
 * @author: WEG
 * @description:
 **/
public class DefaultDocumentAdapter implements IDocumentAdapter, IDocumentListener, IDocumentAdapterExtension {
    private final List<TextChangeListener> fTextChangeListeners = new ArrayList<>();
    private final DocumentEvent fOriginalEvent = new DocumentEvent();
    private final TextChangedEvent changedEvent = new TextChangedEvent();
    private IDocument fDocument;
    private IDocument fDocumentClone;
    private DocumentEvent fEvent;
    private String fOriginalContent;
    private String[] fOriginalLineDelimiters;
    private int fRememberedLengthOfDocument;
    private int fRememberedLengthOfFirstLine;
    private String fLineDelimiter = null;
    private boolean fIsForwarding = true;

    public DefaultDocumentAdapter(){
        setDocument(new Document());
    }

    private static void repairLineInformation(IDocument document) {
        if (document instanceof IRepairableDocument) {
            ((IRepairableDocument) document).repairLineInformation();
        }
    }

    private static String doGetLine(IDocument document, int line) throws BadLocationException {
        IRegion r = document.getLineInformation(line);
        return document.get(r.getOffset(), r.getLength());
    }

    @Override
    public IDocument getDocument() {
        return this.fDocument;
    }

    @Override
    public void setDocument(IDocument document) {
        if (this.fDocument != null) {
            this.fDocument.removePrenotifiedDocumentListener(this);
        }
        this.fDocument = document;
        this.fLineDelimiter = null;
        if (!this.fIsForwarding) {
            this.fDocumentClone = null;
            if (this.fDocument != null) {
                this.fOriginalContent = this.fDocument.get();
                this.fOriginalLineDelimiters = this.fDocument.getLegalLineDelimiters();
            } else {
                this.fOriginalContent = null;
                this.fOriginalLineDelimiters = null;
            }
        }
        if (this.fDocument != null) {
            this.fDocument.addPrenotifiedDocumentListener(this);
        }
    }

    @Override
    public FindReplaceDocumentAdapter find() {
        return new FindReplaceDocumentAdapter(this.fDocument);
    }

    @Override
    public void addTextChangeListener(TextChangeListener listener) {
        Assert.isNotNull(listener);
        if (!this.fTextChangeListeners.contains(listener)) {
            this.fTextChangeListeners.add(listener);
        }
    }

    @Override
    public void removeTextChangeListener(TextChangeListener listener) {
        Assert.isNotNull(listener);
        this.fTextChangeListeners.remove(listener);
    }

    private IDocument getDocumentForRead() {
        if (this.fIsForwarding) {
            return this.fDocument;
        }
        if (this.fDocumentClone == null) {
            this.fDocumentClone = new DocumentClone(this.fOriginalContent == null ? "" : this.fOriginalContent, this.fOriginalLineDelimiters == null ? DefaultLineTracker.DELIMITERS : this.fOriginalLineDelimiters);
        }
        return this.fDocumentClone;
    }

    @Override
    public char getChar(int offset) {
        IDocument document = getDocumentForRead();
        try {
            return document.getChar(offset);
        } catch (BadLocationException e) {
            repairLineInformation(document);
            try {
                return document.getChar(offset);
            } catch (BadLocationException ignored) {
            }
        }
        //unicode
        return '\u0000';
    }

    @Override
    public String getLine(int line) {
        IDocument document = getDocumentForRead();
        try {
            return doGetLine(document, line);
        } catch (BadLocationException e) {
            repairLineInformation(document);
            try {
                return doGetLine(document, line);
            } catch (BadLocationException ignored) {

            }
        }
        return "";
    }

    @Override
    public String getLineDelimiter(int line) {
        IDocument document = getDocumentForRead();
        try {
            return document.getLineDelimiter(line);
        } catch (BadLocationException e) {
            repairLineInformation(document);
            try {
                return document.getLineDelimiter(line);
            } catch (BadLocationException ignored) {
            }
        }
        return "";
    }

    @Override
    public int getLineLength(int line) {
        IDocument document = getDocumentForRead();
        try {
            return document.getLineLength(line);
        } catch (BadLocationException e) {
            repairLineInformation(document);
            try {
                return document.getLineLength(line);
            } catch (BadLocationException ignored) {
            }
        }
        return -1;
    }

    @Override
    public int getLineAtOffset(int offset) {
        IDocument document = getDocumentForRead();
        try {
            return document.getLineOfOffset(offset);
        } catch (BadLocationException e) {
            repairLineInformation(document);
            try {
                return document.getLineOfOffset(offset);
            } catch (BadLocationException ignored) {
            }
        }
        return -1;
    }

    @Override
    public int getLineCount() {
        return getDocumentForRead().getNumberOfLines();
    }

    @Override
    public int getOffsetAtLine(int line) {
        IDocument document = getDocumentForRead();
        try {
            return document.getLineOffset(line);
        } catch (BadLocationException e) {
            repairLineInformation(document);
            try {
                return document.getLineOffset(line);
            } catch (BadLocationException ignored) {
            }
        }
        return -1;
    }

    @Override
    public String getTextRange(int offset, int length) {
        try {
            return getDocumentForRead().get(offset, length);
        } catch (BadLocationException ignored) {
        }
        return "";
    }

    @Override
    public void replaceTextRange(int offset, int length, String text) {
        try {
            this.fDocument.replace(offset, length, text);
        } catch (BadLocationException ignored) {
        }
    }

    @Override
    public void setText(String text) {
        this.fDocument.set(text);
    }

    @Override
    public int getCharCount() {
        return getDocumentForRead().getLength();
    }

    @Override
    public String getLineDelimiter() {
        if (this.fLineDelimiter == null) {
            this.fLineDelimiter = TextUtilities.getDefaultLineDelimiter(this.fDocument);
        }
        return this.fLineDelimiter;
    }

    @Override
    public void documentChanged(DocumentEvent event) {
        if (this.fEvent != null && event == this.fEvent) {
            if (isPatchedEvent(event) || (event.getOffset() == 0 && event.getLength() == this.fRememberedLengthOfDocument)) {
                this.fLineDelimiter = null;
                fireTextSet();
                return;
            }
            if (event.getOffset() < this.fRememberedLengthOfFirstLine) {
                this.fLineDelimiter = null;
            }
            fireTextChanged();
        }
    }

    @Override
    public void documentAboutToBeChanged(DocumentEvent event) {
        this.fRememberedLengthOfDocument = this.fDocument.getLength();
        try {
            this.fRememberedLengthOfFirstLine = this.fDocument.getLineLength(0);
        } catch (BadLocationException e) {
            this.fRememberedLengthOfFirstLine = -1;
        }
        this.fEvent = event;
        rememberEventData(this.fEvent);
        fireTextChanging();
    }

    private boolean isPatchedEvent(DocumentEvent event) {
        return this.fOriginalEvent.fOffset != event.fOffset || this.fOriginalEvent.fLength != event.fLength || !this.fOriginalEvent.fText.equals(event.fText);
    }

    private void rememberEventData(DocumentEvent event) {
        this.fOriginalEvent.fOffset = event.fOffset;
        this.fOriginalEvent.fLength = event.fLength;
        this.fOriginalEvent.fText = event.fText;
    }

    private void fireTextChanged() {
        if (this.fIsForwarding) {
            for (TextChangeListener listener : fTextChangeListeners) {
                listener.textChanged(changedEvent);
            }
        }
    }

    private void fireTextSet() {
        if (this.fIsForwarding) {
            for (TextChangeListener listener : fTextChangeListeners) {
                listener.textSet();
            }
        }
    }

    private void fireTextChanging() {
        int length;
        int i = 0;
        if (this.fIsForwarding) {
            try {
                IDocument document = this.fEvent.getDocument();
                if (document != null) {
                    TextChangingEvent event = new TextChangingEvent();
                    event.start = this.fEvent.fOffset;
                    event.replaceCharCount = this.fEvent.fLength;
                    event.replaceLineCount = document.getNumberOfLines(this.fEvent.fOffset, this.fEvent.fLength) - 1;
                    event.newText = this.fEvent.fText;
                    if (this.fEvent.fText == null) {
                        length = 0;
                    } else {
                        length = this.fEvent.fText.length();
                    }
                    event.newCharCount = length;
                    if (this.fEvent.fText != null) {
                        i = document.computeNumberOfLines(this.fEvent.fText);
                    }
                    event.newLineCount = i;
                    if (this.fTextChangeListeners.size() > 0) {
                        for (TextChangeListener listener : fTextChangeListeners) {
                            listener.textChanging(event);
                        }
                    }

                    changedEvent.start = event.start;
                    changedEvent.replaceCharCount = event.replaceCharCount;
                    changedEvent.replaceLineCount = event.replaceLineCount;
                    changedEvent.newCharCount = event.newCharCount;
                    changedEvent.newText = event.newText;
                }
            } catch (BadLocationException ignored) {
            }
        }
    }

    @Override
    public void resumeForwardingDocumentChanges() {
        this.fIsForwarding = true;
        this.fDocumentClone = null;
        this.fOriginalContent = null;
        this.fOriginalLineDelimiters = null;
        fireTextSet();
    }

    @Override
    public void stopForwardingDocumentChanges() {
        this.fDocumentClone = null;
        this.fOriginalContent = this.fDocument.get();
        this.fOriginalLineDelimiters = this.fDocument.getLegalLineDelimiters();
        this.fIsForwarding = false;
    }

}
